/* accessibility_utils.js --- Utilities for working with the accessible tree

Copyright (C) 2006 by Lukas Loehrer
	This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

  Author: loehrerl [at] gmx [dot] net
*/

// Condition is here so we can load this file even if "module" is not
// available (like in mozrepl)
if (typeof(module) != "undefined") {
	const logger = module.require("package", "lib/filelogger").logger;
	const util = module.require("package", "mozremote/util");
	const dom_util = module.require("package", "dom_util");
}

const Ci = Components.interfaces;

// The following is here for compatibility reasons with older versions of FF 3
var nsIAccessibleRole, nsIAccessibleStates;
if (Ci.nsIAccessibleRole) {
	nsIAccessibleRole = Ci.nsIAccessibleRole;
	nsIAccessibleStates = Ci.nsIAccessibleStates;
} else {
	nsIAccessibleRole = Ci.nsIAccessible;
	nsIAccessibleStates = Ci.nsIAccessible;
}


// {{{Mappings from constant values to their names 

if (typeof(util) != "undefined") {
	const IAccEventNames = util.initConstantNames(Ci.nsIAccessibleEvent, "EVENT_");
	const IAccRoleNames = util.initConstantNames(nsIAccessibleRole, "ROLE_");
	const IAccStateNames = util.initConstantNames(nsIAccessibleStates, "STATE_");
	const IAccExtStateNames = util.initConstantNames(nsIAccessibleStates, "EXT_STATE_");
	const IAccRelationNames = util.initConstantNames(Ci.nsIAccessible, "RELATION_");
}

// }}}



// {{{Finding the nsIAccessible for a DOM node 

function getAccessibleFor2(domNode) {
var acc = Components.classes["@mozilla.org/accessibilityService;1"]                                      
.getService(Components.interfaces.nsIAccessibilityService);              
                                                                       

acc = acc.getAccessibleFor(domNode);
return acc;
/*
.QueryInterface            
(Components.interfaces.nsIAccessNode).accessibleDocument;     
*/

}

function getAccessibleFor(domNode) {
	var accRetrieval = null;
	try {
		accRetrieval = Components.classes["@mozilla.org/accessibleRetrieval;1"].createInstance(Components.interfaces.nsIAccessibleRetrieval);
	}
	catch (ex) { dump ("\nError getting accessibility retrieval service\n" + ex);}

	if (!accRetrieval) {
		alert("No accessible retrieval");
		return null;
	}
   
	try {
		return accRetrieval.getAccessibleFor(domNode);
	} catch(ex) {
}
  
	return null;
}   

// Gets first accessible ancestor
function findAccessibleFor(domNode) {
	var result = getAccessibleFor(domNode);
	while (!result) {
		domNode = domNode.parent;
		result = getAccessibleFor(domNode);
	}
	return result;
}

// }}}
// {{{Getting the DOM node for an nsIAccessible

function getDOMNodeFor(aAccessible) {
	var accNode = aAccessible.QueryInterface(Ci.nsIAccessNode);
	return accNode.DOMNode;
}

// }}}
// {{{Tree traversal

function visitChildNodes(aNode, aVisitor) {
	// aNode is an nsIAccessible
	var result = [];
	try {	
		var child = aNode.firstChild;
		while (child) {
			var childResult = visitNode(child, aVisitor);
			if (childResult != null) {
				result.push(childResult);
			}
			child = child.nextSibling;
		}
	} catch (e) {
		logger.error("Error visiting child nodes: " + e, e);
	}
	return result;
}

function visitNode(aNode, aVisitor) {
	var result = null;
	try {
		var f = aVisitor[aNode.finalRole];
		if (f) {
			result = f.call(aVisitor, aNode);
		} else {
			result = aVisitor.defaultAction(aNode);
		}
	} catch (e) {
		logger.error("visitNode: " + e, e);
	}
	return result;
}

// {{{Accessors guarded by try-catch 

function getFirstChild(aNode) {
	try {
		return aNode.firstChild;
	} catch (e) {
		return null;
	}
}

function getLastChild(aNode) {
	try {
		return aNode.lastChild;
	} catch (e) {
		return null;
	}
}

function getNextSibling(aNode) {
	try {
		return aNode.nextSibling;
	} catch (e) {
		return null;
	}
}

function getPreviousSibling(aNode) {
	try {
		return aNode.previousSibling;
	} catch (e) {
		return null;
	}
}

function getParent(aNode) {
	try {
		// Do not leave the document
		if (aNode.finalRole == nsIAccessible.ROLE_DOCUMENT) {
			return null;
		}
		return aNode.parent;
	} catch (e) {
		return null;
	}
}

// }}}

function getNextNode(aNode, notDown) {
	notDown = notDown || false;
	var next = null;
	
	if (!notDown) {
		next = getFirstChild(aNode);
		if (next) {
			return next;
		}
	}
	
	while (aNode) {	
		next = getNextSibling(aNode);
		if (next) {
			return next;
		}

		aNode = getParent(aNode);
	}
	return null;
}



	// }}}

// {{{Finding nodes

function getDocumentLineage(aNode) {
	var result = [];
	var tempNode = aNode;
	
	while (tempNode && tempNode.role != nsIAccessibleRole.ROLE_DOCUMENT) {
		result.push(tempNode);
		tempNode = tempNode.parent;
	}
	if (tempNode) {
		result.push(tempNode);
	}
	return result.reverse();
}

function getNestingLevel(aNode) {
	return [ n
						  for each (n in getDocumentLineage(aNode))
						  if (n.role == aNode.role) ].length - 1;
}

function FindClosestAncestorWithRole(aStartNode, aRoleList) {
	var node = aStartNode;
	var result = null;

	while (node) {
		if (aRoleList.indexOf(node.finalRole) >= 0) {
			result = node;
			break;
		}
		node = getParent(node);
	}
return result;
}

function findFirstChildWithRole(aNode, aRoleList) {

	var child = getFirstChild(aNode); 
	while (child) {
		if (aRoleList.indexOf(child.finalRole)) {
			return child;
		}
		var childResult = getFirstChildWithRole(child, aRoleList);
		if (childResult) {
			return childResult;
		}
		child = getNextSibling(child);
	}
	return null;
}

/*
  Only works if the roles in aRoleList cannot be nested (e.g. form
  elements)
*/
function findNextWithRole(aStartNode, aRoleList) {
	while (aNode) {
		if (aRoleList.indexOf(aNode.finalRole)) {
			return aNode;
		}
		aNode  = getNextNode(aNode);
	}
	return null;
}

// }}}
// {{{ Getting state and extState

function getStatesOld(aAccessible) {
	return [aAccessible.finalState, aAccessible.extState ];
}

function getStatesNew(aAccessible) {
	var state = {}, extState = {};
	aAccessible.getFinalState(state, extState);
	return [ state.value, extState.value ];
}

var getStates = function (acc) {
	if (typeof(acc.getFinalState) == "function") {
		getStates = getStatesNew;
		return getStatesNew(acc);
		getStates = getStatesOld;
	} else {
		return getStatesOld(acc);
	}
};

// }}}
	
// {{{Accessibility event observer 

function AccessibleEventObserver(aListener, aEventList) {
	this._listener = aListener;
	this._interestingEvents = aEventList || [ Ci.nsIAccessibleEvent.EVENT_REORDER,
											  Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE,
											  Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
											  Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_START ];
	this.register();
}

AccessibleEventObserver.prototype = {
	register: function () {
		var observerService = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService);
		return (observerService.addObserver(this, "accessible-event", false) == Components.results.NS_OK);
	},

unregister: function () {
		var observerService = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService);
		return (observerService.removeObserver(this, "accessible-event") == Components.results.NS_OK);
	},

	// nsIObserver
	observe: function (aSubject, aTopic, aData) {
		try {
			if (aTopic != "accessible-event") {
				return;
			}
			var accEvent = null;
			try {
				accEvent = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
			} catch (e) {
				logger.debug("AccessibleEventObserver.observe: Exiting because cannot query nsIAccessibleEvent.");
				return;
			}
			var type = accEvent.eventType;
			if (this._interestingEvents.indexOf(type)  == -1) {
				// logger.debug("AccessibleEventObserver.observe: Exiting because  of uninteresting event type.");
				return;
			}
			var accessible = accEvent.accessible;
			if (accessible) {
				this._listener.onAccessibleEvent(type, accEvent, accessible);
			} else {
				// logger.debug("AccessibleEventObserver.observe: Exiting nsIAccessible is null");
			}
		} catch (e) {
			logger.error("AccessibleEventObserver.observe: " + e, e);
		}
	},

		// nsISupports
		QueryInterface: function(aIID) {
			if(!aIID.equals(CI.nsISupports) && !aIID.equals(CI.nsIObserver)) {
				throw CR.NS_ERROR_NO_INTERFACE;
			}
			return this;
		}
	};

	// }}}
// {{{Table navigation and information

function _getTableForCell(aCell) {
	return aCell.parent.QueryInterface(Ci.nsIAccessibleTable);
}

function _getTableRow(aTable, aCell) {
	return aTable.getRowAtIndex(aCell.indexInParent);
}

function _getTableColumn(aTable, aCell) {
	return aTable.getColumnAtIndex(aCell.indexInParent);
}

/**
   aCell: an nsiAccessible with role=ROLE_CELL
*/
function getTableCellInfo(aCell) {
	var result = {};
	try {
		var table = _getTableForCell(aCell);
		var DOMNode = getDOMNodeFor(aCell);
		//result.cell = DOMNode;

		result.rows = table.rows;
		result.columns = table.columns;

		result.level = getNestingLevel(table); 
		result.row = _getTableRow(table, aCell);
		result.column = _getTableColumn(table, aCell);

		// Row and column headings
		var tmp = dom_util.getRowHeading(DOMNode);
		if (tmp && tmp != DOMNode) {
			result.rowDesc = tmp;
		}
		tmp = dom_util.getColHeading(DOMNode);
		if (tmp && tmp != DOMNode) {
		result.colDesc = tmp;
	}

		// colSpan and rowSpan
		var colSpan = table.getColumnExtentAt(result.row, result.column);
		var rowSpan = table.getRowExtentAt(result.row, result.column);
		if (rowSpan > 1) {
			result.rowSpan = rowSpan;
		}
		if (colSpan > 1) {
			result.colSpan = colSpan;
		}
	} catch (e) {
		logger.error("accessibility_util.getTableCellInfo: " + e, e);
		return null;
	}
	return result;
}

// Helper function for table movement
function _getTableCellRelativeTo(aCell, deltaRow, deltaColumn) {
	try {
		var aTable = _getTableForCell(aCell);
		var row = _getTableRow(aTable, aCell);
		var column = _getTableColumn(aTable, aCell);

		var newRow = row + deltaRow;
		var newColumn = column + deltaColumn;

		if (newRow < 0 || newColumn < 0 ||
			newColumn >= aTable.columns ||
			newRow >= aTable.rows) {
			logger.debug("Movement out of table: " + newRow + "," + newColumn);
			return null;
		}
		return aTable.cellRefAt(newRow, newColumn);
		logger.debug("Error in _getTableCellRelativeTo");
	} catch (e) {
		return null;
	}
}

function getTableCellAbove(aCell) {
	var targetCell = _getTableCellRelativeTo(aCell, -1, 0);
	return  targetCell;
}

function getTableCellBelow(aCell) {
	var accTable = _getTableForCell(aCell);
	var rowSpan = accTable.getRowExtentAt(_getTableRow(accTable, aCell), _getTableColumn(accTable, aCell));
	var targetCell = _getTableCellRelativeTo(aCell, rowSpan, 0);
	return  targetCell;
}

function getTableCellLeft(aCell) {
	var targetCell = _getTableCellRelativeTo(aCell, 0, -1);
	return  targetCell;
}

function getTableCellRight(aCell) {
	var accTable = _getTableForCell(aCell);
	var colSpan = accTable.getColumnExtentAt(_getTableRow(accTable, aCell), _getTableColumn(accTable, aCell));
	var targetCell = _getTableCellRelativeTo(aCell, 0, colSpan);
	return  targetCell;
}

// }}}

// {{{Checking status flags

function checkStateFlag(aNode, aFlagName) {
	return (getStates(aNode)[0] & nsIAccessibleStates["STATE_" + aFlagName]);
}

function checkExtStateFlag(aNode, aFlagName) {
	return (getStates(aNode)[1] & nsIAccessibleStates["EXT_STATE_" + aFlagName]);
}

	// }}}


// {{{ Emacs local variables

// local variables:
// folded-file: t
// end:

// }}}
